﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Remoting.Proxies;

namespace WMemoryProfiler.Memory
{
    /// <summary>
    /// 
    /// </summary>
    public class ObjectGraph
    {
        object[] Objects;
        Dictionary<Type, FieldInfo[]> TypeFields = new Dictionary<Type, FieldInfo[]>();

        /// <summary>
        /// 
        /// </summary>
        /// <param name="objects"></param>
        public ObjectGraph(object [] objects)
        {
            ReloadGraph(objects);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="objects"></param>
        public void ReloadGraph(object[] objects)
        {
            if (objects == null)
            {
                throw new ArgumentNullException("objects");
            }
            Objects = objects.Where(o => !(o is RealProxy)).ToArray();

            HashSet<Type> types = new HashSet<Type>(Objects.Select(o => o.GetType()));
            foreach(var t in types)
            {
                AddAllFieldTypes(t);
            }

        }

        /// <summary>
        /// Add all non primitive field types
        /// </summary>
        /// <param name="t"></param>
        void AddAllFieldTypes(Type t)
        {
            if(TypeFields.ContainsKey(t) )
            {
                return;
            }

            var typeFields = GetAllFields(t).ToArray();
            TypeFields.Add(t, typeFields.Where(IsRelevantFieldType).ToArray());

            
            // value types are normally not stored on the GC heap. We need therefore to traverse value type fields
            // as well
            foreach (var fieldType in typeFields.Where(IsRelevantFieldType)
                                                .Select(field => field.FieldType))
            {
                if (TypeFields.ContainsKey(fieldType))
                    continue;
                AddAllFieldTypes(fieldType);
            }
        }

        bool IsRelevantFieldType(FieldInfo fieldType)
        {
            return !fieldType.FieldType.IsPrimitive && !fieldType.FieldType.IsArray && !typeof(RealProxy).IsAssignableFrom(fieldType.FieldType) && fieldType.FieldType.Name != typeof(SignatureHelper).Name;

        }

        IEnumerable<FieldInfo> GetAllFields(Type t)
        {
            if (t == null)
                return Enumerable.Empty<FieldInfo>();

            BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly;
            return t.GetFields(flags).Union(GetAllFields(t.BaseType));
        }

        /// <summary>
        /// Get roots up a certain number and level.
        /// </summary>
        /// <param name="obj">Object to get roots from</param>
        /// <param name="maxDepth">Maximum number of levels before the traverals does stop</param>
        /// <param name="maxParents">Maximum number of parents per object level that are collected</param>
        /// <returns>An ObjectNode which does contain the input object. Then you can travers the parents of it to see where it is rooted.</returns>
        public ObjectNode GetRoots(object obj, int maxDepth = 50, int maxParents = 500)
        {
            var startNode = new ObjectNode(obj, null);
            ObjectNode[] objects = new ObjectNode[] { startNode };
            HashSet<object> visited = new HashSet<object>();
            visited.Add(obj);

            for (int i = 0; i < maxDepth; i++)
            {
                if (objects.Length == 0)
                { 
                    break;
                }

                foreach(var start in objects)
                {
                    var parents = GetParents(start.Object);
                    foreach (var parent in parents)
                    {
                        start.AddParent(visited, parent);    
                    }
                }
            }

            return startNode;
        }


        /// <summary>
        /// Get first level parents of an object.
        /// </summary>
        /// <param name="obj">Object to get parents from</param>
        /// <param name="maxParents">Maximum number of parents</param>
        /// <returns>Array of ObjectNodes which do contain the field and object.</returns>
        public ObjectNode[] GetParents(object obj, int maxParents=500)
        {
            HashSet<KeyValuePair<object, FieldInfo>> foundParents = new HashSet<KeyValuePair<object, FieldInfo>>();
            foreach (var potentialParent in Objects)
            {
                if (potentialParent is Array)
                {
                    VisitArray(foundParents, (Array)potentialParent, obj);
                }
                else
                { 
                    VisitFields(new HashSet<object>(), foundParents, potentialParent, obj);
                }
                if (foundParents.Count >= 500)
                { 
                    break;
                }
            }

            return foundParents.Select(p => new ObjectNode(p.Key, p.Value)).ToArray();
        }

        private void VisitArray(HashSet<KeyValuePair<object, FieldInfo>> foundParents, Array array, object obj)
        {
            // Arrays of int, float, ... cannot contain references to objects
            if (array.GetType().GetElementType().IsPrimitive)
                return;

            foreach (var arrayItem in array)
            {
                if (object.ReferenceEquals(obj, arrayItem))
                {
                    foundParents.Add(new KeyValuePair<object,FieldInfo>(array,null));
                    break;
                }
            }
        }

        void VisitFields(HashSet<object> visited, HashSet<KeyValuePair<object, FieldInfo>> foundParents, object potentialParent, object obj)
        {
            foreach (var field in TypeFields[potentialParent.GetType()])
            {
                var objRef = field.GetValue(potentialParent);
                if (objRef == null)
                { 
                    continue;
                }

                try
                {
                    if (visited.Contains(objRef))
                    {
                        return;
                    }
                    visited.Add(objRef);


                    // traverse value types because these are usually not as objects on the heap visible
                    if (field.FieldType.IsValueType)
                    {
                        VisitFields(visited, foundParents, objRef, obj);
                    }
                }
                finally
                {
                    if (Object.ReferenceEquals(objRef, obj))
                    {
                        foundParents.Add(new KeyValuePair<object,FieldInfo>(potentialParent, field));
                    }
                }
            }
        }
    }
}
